Guidelines for integrating Convex real-time database into the RFP Discovery application
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: Convex Integration description: Guidelines for integrating Convex real-time database into the RFP Discovery application
Convex Integration Skill
This skill provides guidance for integrating Convex as the real-time database backend for the RFP Discovery application.
Installation
npm install convex
npx convex dev
Schema Design
Create convex/schema.ts with the following tables:
RFP Table
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
rfps: defineTable({
// Core fields
externalId: v.string(), // ID from source platform
source: v.string(), // "sam.gov", "rfpmart", "emma", etc.
title: v.string(),
summary: v.string(),
url: v.string(),
// Dates
postedDate: v.optional(v.string()),
deadline: v.optional(v.string()),
questionDeadline: v.optional(v.string()),
// Location/Category
location: v.optional(v.string()),
category: v.optional(v.string()),
state: v.optional(v.string()),
country: v.optional(v.string()),
// Budget
budget: v.optional(v.string()),
// Eligibility
eligibility: v.optional(v.string()),
isUsaOnly: v.optional(v.boolean()),
requiresOnshore: v.optional(v.boolean()),
setAsideType: v.optional(v.string()),
// Metadata
fetchedAt: v.number(),
rawData: v.optional(v.string()),
}).index("by_external_id", ["externalId", "source"])
.index("by_source", ["source"])
.index("by_deadline", ["deadline"]),
evaluations: defineTable({
rfpId: v.id("rfps"),
userId: v.optional(v.string()), // Clerk user ID
// Overall result
isFit: v.boolean(),
score: v.number(),
maxScore: v.number(),
// Per-criterion results
technicalRelevance: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
scopeFit: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
categoryFocus: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
clientProfile: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
logistics: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
skillSetAlignment: v.object({
met: v.boolean(),
details: v.optional(v.string()),
}),
// AI analysis
aiProvider: v.optional(v.string()),
aiAnalysis: v.optional(v.string()), // JSON string
reasoning: v.optional(v.string()),
// Timestamps
evaluatedAt: v.number(),
}).index("by_rfp", ["rfpId"])
.index("by_user", ["userId"]),
pursuits: defineTable({
rfpId: v.id("rfps"),
userId: v.string(), // Clerk user ID
// Pipeline stage
stage: v.union(
v.literal("new"),
v.literal("triage"),
v.literal("bid"),
v.literal("no_bid"),
v.literal("capture"),
v.literal("submitted"),
v.literal("won"),
v.literal("lost")
),
// Decision tracking
decision: v.optional(v.union(
v.literal("pursue"),
v.literal("partner_needed"),
v.literal("reject")
)),
decisionReason: v.optional(v.string()),
// Notes
notes: v.optional(v.string()),
// Timestamps
createdAt: v.number(),
updatedAt: v.number(),
}).index("by_user", ["userId"])
.index("by_stage", ["stage"]),
userSettings: defineTable({
userId: v.string(), // Clerk user ID
// AI Settings
selectedAiProvider: v.string(),
aiProviderConfigs: v.optional(v.string()), // JSON
corePromptTemplate: v.optional(v.string()),
useAiForEvaluation: v.boolean(),
// Criteria Config
criteriaConfig: v.optional(v.string()), // JSON
// Refresh Settings
autoRefreshIntervalHours: v.number(),
// UI Preferences
theme: v.union(v.literal("light"), v.literal("dark")),
}).index("by_user", ["userId"]),
});
Query Patterns
Fetching RFPs with Evaluations
// convex/rfps.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const listWithEvaluations = query({
args: {
source: v.optional(v.string()),
limit: v.optional(v.number())
},
handler: async (ctx, args) => {
let rfpsQuery = ctx.db.query("rfps");
if (args.source) {
rfpsQuery = rfpsQuery.withIndex("by_source", (q) =>
q.eq("source", args.source)
);
}
const rfps = await rfpsQuery
.order("desc")
.take(args.limit ?? 50);
// Fetch evaluations for each RFP
const rfpsWithEvals = await Promise.all(
rfps.map(async (rfp) => {
const evaluation = await ctx.db
.query("evaluations")
.withIndex("by_rfp", (q) => q.eq("rfpId", rfp._id))
.first();
return { ...rfp, evaluation };
})
);
return rfpsWithEvals;
},
});
Mutation Patterns
Saving an Evaluation
// convex/evaluations.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const saveEvaluation = mutation({
args: {
rfpId: v.id("rfps"),
isFit: v.boolean(),
score: v.number(),
maxScore: v.number(),
criteriaResults: v.object({
technicalRelevance: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
scopeFit: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
categoryFocus: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
clientProfile: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
logistics: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
skillSetAlignment: v.object({ met: v.boolean(), details: v.optional(v.string()) }),
}),
aiProvider: v.optional(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
return await ctx.db.insert("evaluations", {
rfpId: args.rfpId,
userId: identity?.subject,
isFit: args.isFit,
score: args.score,
maxScore: args.maxScore,
...args.criteriaResults,
aiProvider: args.aiProvider,
evaluatedAt: Date.now(),
});
},
});
Integration with Clerk
When using Convex with Clerk, configure authentication in convex/auth.config.js:
export default {
providers: [
{
domain: "https://your-clerk-domain.clerk.accounts.dev",
applicationID: "convex",
},
],
};
React Hooks Usage
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function RfpList() {
const rfps = useQuery(api.rfps.listWithEvaluations, { limit: 50 });
const saveEvaluation = useMutation(api.evaluations.saveEvaluation);
// Component implementation
}
Migration Strategy
-
Phase 1: Add Convex alongside existing localStorage
- Create Convex schema
- Add mutations to sync localStorage to Convex
- Keep localStorage as fallback
-
Phase 2: Migrate reads to Convex
- Replace localStorage reads with Convex queries
- Add real-time subscriptions
-
Phase 3: Remove localStorage
- Remove localStorage sync code
- Use Convex as single source of truth
More by Atemndobs
View allIngest RFP opportunities from multiple data sources (SAM.gov, eMMA, RFPMart). Use when adding new data sources, modifying ingestion logic, or debugging data fetching issues.
Generate and track compliance matrices for RFP requirements. Use when building requirements tracking features, implementing compliance checklists, or ensuring proposal coverage.
Convex database patterns and best practices for RFP Discovery. Use when writing Convex queries, mutations, actions, or schema definitions. Also helpful for real-time subscriptions and auth integration.
Guidelines for integrating additional RFP data sources beyond RFPMart
